package org.javanum.number;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;

import java.math.BigDecimal;
import java.math.MathContext;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Random;

import static junit.framework.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeNotNull;
import static org.junit.Assume.assumeTrue;

/**
 * Copyright 2010 Scott Fines
 * <p/>
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p/>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p/>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * <p/>
 * Author: Scott Fines
 * Date: Jan 24, 2010
 */
@RunWith(Parameterized.class)
public class BigTaylorTest {

    private static final String sinErrorMsg =
                            "sin(%1$s)!=%2$s to within the " +
                        "specified accuracy. Expected=%2$s. Actual=%3$s.";

    private final BigDecimal element;
    private final MathContext testPrecision;
    private static final String cosErrorMsg =
                        "cos(%1$s)!=%2$s to within the " +
                        "specified accuracy. Expected=%2$s. Actual=%3$s.";

    public BigTaylorTest(BigDecimal element, MathContext testPrecision) {
        this.element = element;
        this.testPrecision = testPrecision;
    }

    @Parameterized.Parameters
    public static Collection<Object[]> getParameters(){
         Collection<Object[]> objects = new ArrayList<Object[]>();

        int maxPrecision = 30;
        Random random = new Random();
        for(int i=30;i<=maxPrecision;i+=10){
            MathContext context = new MathContext(i);
            objects.add(new Object[]{BigDecimal.ZERO,context});
            objects.add(new Object[]{BigDecimal.ONE,context});

            double randomValue = random.nextDouble()*i;
            objects.add(new Object[]{new BigDecimal(randomValue),context});
        }

        return objects;
    }

    @Test
    public void testSin() throws Exception {
        BigDecimal x = element;
        assumeNotNull(x,testPrecision);

        //test that it is accurate to at least double precision
        BigDecimal sinX = BigTaylor.sin(x,testPrecision);
        double mathSin = Math.sin(x.doubleValue());
        assertEquals(String.format(sinErrorMsg,x,mathSin,sinX),
                    mathSin,
                    sinX.doubleValue(),
                    Math.pow(1.0,-10));
    }

    @Test
    public void testPythagoreanIdentity()throws Exception{
        BigDecimal x = element;
        assumeNotNull(x,testPrecision);

        BigDecimal sinX = BigTaylor.sin(x,testPrecision);
        BigDecimal cosX = BigTaylor.cos(x,testPrecision);
        BigDecimal pythagoras = sinX.pow(2).add(cosX.pow(2));
        assertTrue(String.format("Pythagorean identity failed: " +
                "Expected 1. Actual=%1$s",pythagoras),
                BigMath.equalToWithinPrecision(BigDecimal.ONE,
                                               pythagoras,
                                           testPrecision.getPrecision()));

    }

    @Test
    public void testSinSpecialValues() {
        //test special values;
        assumeNotNull(testPrecision);
        BigDecimal PI = BigMath.pi(testPrecision);
        BigDecimal TWO_PI = BigMath.TWO.multiply(PI);
        BigDecimal PI_OVER_2 = PI.divide(BigMath.TWO);
        BigDecimal THREE_PI_OVER_2 = new BigDecimal("3").
                                        multiply(PI_OVER_2);

        BigDecimal result = BigTaylor.sin(BigDecimal.ZERO,testPrecision);
        assertTrue(String.format(sinErrorMsg,0,0,result),
                BigMath.equalToWithinPrecision(
                        result,
                        BigDecimal.ZERO,
                        testPrecision.getPrecision()));

        result = BigTaylor.sin(PI,testPrecision);
        assertTrue(String.format(sinErrorMsg,PI,0,result),
                        BigMath.equalToWithinPrecision(
                                result,
                                BigDecimal.ZERO,
                                testPrecision.getPrecision()));

        result = BigTaylor.sin(TWO_PI,testPrecision);
        assertTrue(String.format(sinErrorMsg,TWO_PI,0,result),
                        BigMath.equalToWithinPrecision(
                                result,
                                BigDecimal.ZERO,
                                testPrecision.getPrecision()));

        result = BigTaylor.sin(PI_OVER_2,testPrecision);
        assertTrue(String.format(sinErrorMsg,PI_OVER_2,1,result),
                        BigMath.equalToWithinPrecision(
                                result,
                                BigDecimal.ONE,
                                testPrecision.getPrecision()));

        result = BigTaylor.sin(THREE_PI_OVER_2,testPrecision);
        assertTrue(String.format(sinErrorMsg,THREE_PI_OVER_2,-1,result),
                        BigMath.equalToWithinPrecision(
                                result,
                                BigDecimal.ONE.negate(),
                                testPrecision.getPrecision()));
    }

    @Test
    public void testCosSpecialValues() {
        //test special values;
        assumeNotNull(testPrecision);
        BigDecimal PI = BigMath.pi(testPrecision);
        BigDecimal TWO_PI = BigMath.TWO.multiply(PI);
        BigDecimal PI_OVER_2 = PI.divide(BigMath.TWO);
        BigDecimal THREE_PI_OVER_2 = new BigDecimal("3").
                                        multiply(PI_OVER_2);

        BigDecimal result = BigTaylor.cos(BigDecimal.ZERO,testPrecision);
        assertTrue(String.format(cosErrorMsg,0,1,result),
                BigMath.equalToWithinPrecision(
                        result,
                        BigDecimal.ONE,
                        testPrecision.getPrecision()));

        result = BigTaylor.cos(PI,testPrecision);
        assertTrue(String.format(cosErrorMsg,PI,-1,result),
                        BigMath.equalToWithinPrecision(
                                result,
                                BigDecimal.ONE.negate(),
                                testPrecision.getPrecision()));

        result = BigTaylor.cos(TWO_PI,testPrecision);
        assertTrue(String.format(cosErrorMsg,TWO_PI,1,result),
                        BigMath.equalToWithinPrecision(
                                result,
                                BigDecimal.ONE,
                                testPrecision.getPrecision()));

        result = BigTaylor.cos(PI_OVER_2,testPrecision);
        assertTrue(String.format(cosErrorMsg,PI_OVER_2,0,result),
                        BigMath.equalToWithinPrecision(
                                result,
                                BigDecimal.ZERO,
                                testPrecision.getPrecision()));

        result = BigTaylor.cos(THREE_PI_OVER_2,testPrecision);
        assertTrue(String.format(cosErrorMsg,THREE_PI_OVER_2,0,result),
                        BigMath.equalToWithinPrecision(
                                result,
                                BigDecimal.ZERO,
                                testPrecision.getPrecision()));
    }

    @Test
    public void testCos() throws Exception {
        BigDecimal x = element;
        assumeNotNull(x,testPrecision);

        //test that it is accurate to at least double precision
        BigDecimal cosX = BigTaylor.cos(x,testPrecision);
        double mathSin = Math.cos(x.doubleValue());
        assertEquals(String.format(cosErrorMsg,x,mathSin,cosX),
                    mathSin,
                    cosX.doubleValue(),
                    Math.pow(1.0,-10));
    }

    @Test
    public void testTan() throws Exception {
        /*
        This method is a wrapper around sin and cos methods, with
        additional checks for certain special values for efficiency and
        error checking. Thus, we need not write a general purpose
        test for this method, as it will be necessarily correct if
        sin and cos are.
         */
    }

    @Test
    public void testTanSpecialValues() throws Exception{
        MathContext prec = testPrecision;
        BigDecimal PI = BigMath.pi(prec);
        BigDecimal ZERO = BigDecimal.ZERO;
        BigDecimal piOver4 = PI.divide(new BigDecimal("4.0"),testPrecision);

        BigDecimal tanZERO = BigTaylor.tan(ZERO,prec);
        assertTrue(String.format("tan(0)!=0. Expected=%1$s. Actual=%2$s",
                                    BigDecimal.ZERO,tanZERO),
                BigMath.equalToWithinPrecision(BigDecimal.ZERO,tanZERO,
                                                prec.getPrecision()));

        BigDecimal tanPI = BigTaylor.tan(ZERO,prec);
        assertTrue(String.format("tan(pi)!=0. Expected=%1$s. Actual=%2$s",
                                            BigDecimal.ZERO,tanPI),
                        BigMath.equalToWithinPrecision(BigDecimal.ZERO,tanPI,
                                                        prec.getPrecision()));

        BigDecimal tanPIOver4 = BigTaylor.tan(piOver4,prec);
        assertTrue(String.format("tan(pi/4)!=1. Expected=%1$s. Actual=%2$s",
                                            BigDecimal.ONE,tanPIOver4),
                        BigMath.equalToWithinPrecision(
                                BigDecimal.ONE,tanPIOver4,
                                                  prec.getPrecision()));

        BigDecimal tanPIOver4neg = tanPIOver4.negate();
        assertTrue(String.format("tan(-pi/4)!=-1. Expected=%1$s. Actual=%2$s",
                                            BigDecimal.ONE.negate(),
                                            tanPIOver4neg),
                        BigMath.equalToWithinPrecision(
                                BigDecimal.ONE.negate(),tanPIOver4neg,
                                                    prec.getPrecision()));

    }

    @Test(expected=ArithmeticException.class)
    public void testTanThrowsException() throws Exception {
        BigDecimal input = BigMath.pi(testPrecision).divide(BigMath.TWO);

        BigDecimal result = BigTaylor.tan(input,testPrecision);
    }

    @Test
    public void testExp() throws Exception {
        /*
        This method is better tested in combination with
        testing the natural logarithm function, as an inversion
        test.

        Therefore, this method only tests a few special
        cases such as when the input is BigDecimal.ONE or
        BigDecimal.ZERO, and that e^x>0 for all x.
        The remainder of the correctness test
        will be accomplished in testExpLnInversion()
         */

//        BigDecimal result = BigTaylor.exp(BigDecimal.ONE,testPrecision);
//        BigDecimal E = BigMath.e(testPrecision);
//        assertTrue(String.format("e^(1)!=e. Expected=%1$s. Actual=%2$s",
//                                    E,result),
//                BigMath.equalToWithinPrecision(
//                        E,result,
//                        testPrecision.getPrecision()));

        BigDecimal shouldBeOne = BigTaylor.exp(BigDecimal.ZERO,testPrecision);
        assertTrue(String.format("e^(0)!=1. Expected=%1$s. Actual=%2$s",
                                BigDecimal.ONE,shouldBeOne),
                        BigMath.equalToWithinPrecision(
                                BigDecimal.ONE,shouldBeOne,
                                testPrecision.getPrecision()));

        BigDecimal exp = BigTaylor.exp(element,testPrecision);
        assertTrue(String.format("exp(%1$s)<=0!",element),
                    exp.compareTo(BigDecimal.ZERO)>0);
    }

    @Test
    public void testE() throws Exception {
        /*
        Tests that e is accurate to within 40 decimal places.
        Chances are that if the method is correct to within 40
        decimal places, it will be correct to within more.

        The value taken to represent e to 40 decimal places is
        taken from Knuth[7]

        TODO -sf- improve this test for increased betterness
         */
        BigDecimal knuthE =
            new BigDecimal("2.7182818284590452353602874713526624977572");
        MathContext knuthPrecision = new MathContext(40);
        BigDecimal myE = BigTaylor.e(knuthPrecision);

        assertTrue(String.format(
                "e incorrect%n" +
               " Expected=%1$s%n" +
               "   Actual=%2$s",knuthE,myE),
                BigMath.equalToWithinPrecision(
                        myE,knuthE,knuthPrecision.getPrecision()));
    }

    @Test
    public void testExpLnInversion() throws Exception{
        BigDecimal x = element;
        assumeNotNull(x,testPrecision);

        BigDecimal exp = BigTaylor.exp(x,testPrecision);
        BigDecimal lnExp = BigTaylor.ln(exp,new MathContext(2*testPrecision.getPrecision()));
        BigDecimal diff = lnExp.subtract(x).abs();
        assertTrue(String.format("ln(exp(x))!=x.%n " +
                "|ln(e^x)-x|=%1$s %n" +
                "Context=%2$s",diff,testPrecision),
                BigMath.equalToWithinPrecision(x,lnExp,
                        testPrecision.getPrecision()));

        assumeTrue(BigDecimal.ZERO.compareTo(x)<0);
        BigDecimal ln = BigTaylor.ln(x,testPrecision);
        BigDecimal expLn = BigTaylor.exp(ln,new MathContext(2*testPrecision.getPrecision()));
        diff = expLn.subtract(x).abs();
        assertTrue(String.format("exp(ln(x))!=x. %n" +
                        "|e^(ln(x))-x|=%1$s %n" +
                        "Context=%2$s",diff,testPrecision),
                        BigMath.equalToWithinPrecision(x,expLn,
                                testPrecision.getPrecision()));
    }

    @Test
    public void testLn() throws Exception {
        /*
        this method is better tested as an interaction
        with a test for the exponential function, as an
        inversion-test (see the notes for testExp()).

        Therefore, this method will only test the
        special inputs 1 and e.
         */

//        assumeNotNull(testPrecision);
//        BigDecimal E = BigMath.e(testPrecision);
//        BigDecimal lnE = BigTaylor.ln(E,testPrecision);
//        assertTrue(String.format("ln(e)!=1. Expected=%1$s, Actual=%2$s",
//                                BigDecimal.ONE,lnE),
//                    BigMath.equalToWithinPrecision(
//                            BigDecimal.ONE,lnE,
//                            testPrecision.getPrecision()));

        BigDecimal lnOne = BigTaylor.ln(BigDecimal.ONE,testPrecision);
        assertTrue(String.format("ln(1)!=0. Expected=%1$s, Actual=%2$s",
                                BigDecimal.ZERO,lnOne),
                    BigMath.equalToWithinPrecision(
                            BigDecimal.ZERO, lnOne,
                            testPrecision.getPrecision()));
    }

    @Test(expected=ArithmeticException.class)
    public void testLnDoesNotLikeNegatives() throws Exception {
        assumeNotNull(element,testPrecision);
        BigDecimal x =
                element.compareTo(BigDecimal.ZERO)<=0?
                        element:element.negate();

        BigDecimal ln = BigTaylor.ln(x,testPrecision);
    }
}
